Main.php

<?php

namespace Lia\App\MdBlog;

use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
use League\CommonMark\Extension\CommonMark\Node\Block\IndentedCode;
use League\CommonMark\MarkdownConverter;
use Spatie\CommonMarkHighlighter\FencedCodeRenderer;
use Spatie\CommonMarkHighlighter\IndentedCodeRenderer;

/**
 * Add caching of html
 */
class Main extends \Lia\Addon {

    public $name = 'blog';
    public $fqn = 'tlf:mdblog.blog';

    public $dir;

    public function __construct($package=null){
        parent::__construct($package);

        $this->dir = &$this->props['dir'];
        // $this->prefixes['on']('test is yes',null,null);
        // exit;
        $this->scan('on', $this);
    }
    public function init(){
        if (!isset($this->dir))throw new \Exception("You must set mdblog.blog.dir");
        $this->scan('route');
    }

    public function onServerStart(){
        $this->init();
    }

    /**
     *
     * @todo test seo title
     * @todo add: seoDescription
     */
    public function routeBlogs($route,$response=null){
        if ($route===false)return $this->get_routes();

        $url = $route->url();
        $url = substr($url,strlen($this->lia->base_url), -1);

        $response->content = $this->get_blog_html($url);
        preg_match('/<h1>([^<]+)<\/h1>/', $response->content, $matches);
        if (isset($matches[1])){
            $this->seoTitle($matches[1]);
        }
    }

    public function get_routes(){
        $urls = [];
        $blogs = $this->get_all_blogs();
        foreach ($blogs as $category=>$list){
            foreach ($list as $blog){
                $urls[] = $blog['url'];
            }
        }
        return $urls;
    }

    /**
     * convert markdown to html
     */
    public function md_to_html(string $markdown){
        $environment = new Environment();
        $environment->addExtension(new CommonMarkCoreExtension());
        $environment->addRenderer(FencedCode::class, new FencedCodeRenderer());
        $environment->addRenderer(IndentedCode::class, new IndentedCodeRenderer());

        $markdownConverter = new MarkdownConverter($environment);

        return $markdownConverter->convertToHtml($markdown);
    }

    /**
     * @param $name the `category/slug` of the post
     */
    public function get_blog_html($name){
        $file = $this->dir .'/' . $name.'.md';

        $cache_file = __DIR__.'/cache/'.$name.'.html';
        if (!is_file($cache_file) || filemtime($file) > filemtime($cache_file)){
            $content = file_get_contents($file);
            $html = $this->md_to_html($content).''; 
            $dir = dirname($cache_file);
            if (!is_dir($dir))mkdir($dir, 0754, true);
            file_put_contents($cache_file, $html);
            return $html;
        }

        return file_get_contents($cache_file);
    }

    /**
     *
     * 
     * @param $rel the relative path inside $dir
     * @param $dir the base dir to search in
     * @return array of relative paths that do NOT include `$dir` in the string
     */
    public function get_all_dirs($rel, $dir){
        $dirs = [];
        foreach (scandir($dir.'/'.$rel) as $f){
            if ($f == '.' || $f == '..')continue;
            if (!is_dir($dir.'/'.$rel.'/'.$f))continue;
            $dirs[] = $rel.$f.'/';
            $sub = $this->get_all_dirs($rel.$f.'/', $dir);
            foreach ($sub as $s){$dirs[] = $s;}
        }

        return $dirs;
    }

    public function get_all_blogs(array $categories=[], ?callable $sorter=null){
        if (!isset($this->dir))throw new \Exception("You need to set mdblog.blog.dir");
        $blogs = [];
        foreach ($this->get_all_dirs('', $this->dir) as $rel){
            $category = substr($rel,0,-1);
            if ($categories!==[]&&!in_array($category, $categories)){
                continue;
            }
            $entries = $this->get_blog_entries($category);
            if (count($entries)==0)continue;
            $blogs[$category] = $entries;
        }
        if (is_callable($sorter)){
            $blogs = $sorter($blogs);
        }
        return $blogs;
    }

    /**
     * @param array $categories different categories to show or empty array for all
     * @param $sorter a callable that gets an array of blogs & returns an array of blogs. @see(Lia\Addon\MdBlog\Test\Main::testGridSorter())
     */
    public function get_blog_grid(array $categories=[], ?callable $sorter=null){
        
        $blogs = $this->get_all_blogs($categories, $sorter);

        $content = $this->view('Blog/CardGrid', ['blogs'=>$blogs, 'blogger'=>$this]);

        return $content;
    } 

    public function get_blog_card($category){
        $blogs = $this->get_blog_entries($category);
        $content = $this->view('Blog/Card', ['blogList'=>$blogs, 'blogger'=>$this, 'category'=>$category]);

        return $content;
    } 

    public function get_blog_entries($category){
        $dir = $this->dir.'/'.$category; 
        $entries = [];
        foreach (scandir($dir) as $file){
            if (substr($file,-3)!='.md')continue;
            $path = $dir.'/'.$file;
            $name = substr($file,0,-3);
            if (substr($name,-6)=='.draft')continue;
            $entries[] = [
                'title'=>$this->get_title($category.'/'.$name, $path),
                'url'=>$this->lia->base_url.'/'.$category.'/'.$name.'/',
                'path'=>$path,
            ];
        }

        return $entries;
    }


    /**
     * get the title of the given post. $path is used for loading, $name is used for caching?
     * @param $name the name of the blog post, used for caching
     * @param $path the path of the blog post, used for loading the title (not-from-cache)
     */
    public function get_title($name, $path){
        $title = $this->cache_get('mdblog.title:'.$name,false);
        if ($title!=false)return $title;
        $fh = fopen($path, 'r');
        $i=0;
        while (($line = fgets($fh))&&$i++<5){
            if (substr($line,0,2)=='# '){
                $title = trim(substr($line,2));
                $this->cache('mdblog.title:'.$name, $title);
                return $title;
                break;
            }
        }
        fclose($fh);

        return 'no-title';
    }

    /**
     * convert a slug into a readable title
     */
    public function titleFormat($name){
        if (trim($name)=='')$name = 'No Category';
        // $name = 
        $name = implode(' ', explode('-',$name));
        $name = ucwords($name);
        return $name;
    }
}